﻿using System;
using System.ComponentModel;
using System.Linq;
using System.Reflection;
using Database.Core.Attributes;
using Database.Core.Exceptions;

namespace Database.Core
{
  public static class Utilities
  {
    public static object GetDefault(Type type)
    {
      return type.IsValueType
        ? Activator.CreateInstance(type)
        : null;
    }

    /// <summary>
    ///   Returns an Object with the specified Type and whose value is equivalent to the specified object.
    /// </summary>
    /// <param name="value">An Object that implements the IConvertible interface.</param>
    /// <param name="propertyInfo"><see cref="PropertyInfo" /> of property to populate with supplied value.</param>
    /// <returns>
    ///   An object whose Type is conversionType (or conversionType's underlying type if conversionType
    ///   is Nullable&lt;&gt;) and whose value is equivalent to value. -or- a null reference, if value is a null
    ///   reference and conversionType is not a value type.
    /// </returns>
    public static object ChangeType(object value, PropertyInfo propertyInfo)
    {
      object newValue = null;

      try
      {
        var conversionType = propertyInfo.PropertyType;

        // Note: This if block was taken from Convert.ChangeType as is, and is needed here since we're
        // checking properties on conversionType below.
        if(conversionType == null)
        {
          throw new ArgumentNullException("conversionType");
        } // end if

        // If it's not a nullable type, just pass through the parameters to Convert.ChangeType

        if(conversionType.IsGenericType
           &&
           conversionType.GetGenericTypeDefinition() == typeof(Nullable<>))
        {
          // It's a nullable type, so instead of calling Convert.ChangeType directly which would throw a
          // InvalidCastException (per http://weblogs.asp.net/pjohnson/archive/2006/02/07/437631.aspx),
          // determine what the underlying type is
          // If it's null, it won't convert to the underlying type, but that's fine since nulls don't really
          // have a type--so just return null
          // Note: We only do this check if we're converting to a nullable type, since doing it outside
          // would diverge from Convert.ChangeType's behavior, which throws an InvalidCastException if
          // value is null and conversionType is a value type.
          if(value == null)
          {
            return null;
          } // end if

          // It's a nullable type, and not null, so that means it can be converted to its underlying type,
          // so overwrite the passed-in conversion type with this underlying type
          var nullableConverter = new NullableConverter(conversionType);
          conversionType = nullableConverter.UnderlyingType;
        } // end if

        // Now that we've guaranteed conversionType is something Convert.ChangeType can handle (i.e. not a
        // nullable type), pass the call on to Convert.ChangeType
        if(conversionType.IsEnum)
        {
          if(value is int)
          {
            return Enum.ToObject(propertyInfo.PropertyType, value);
          }

          return GetEnumValue(propertyInfo, (string)value);
        }

        newValue = Convert.ChangeType(value, conversionType);
      }
      catch(Exception ex)
      {
        throw new TypeConversionException(propertyInfo, "Type Conversion Failed", ex);
      }

      return newValue;
    }

    /// <summary>
    ///   Creates an enumerated value type.
    /// </summary>
    /// <param name="propertyInfo"></param>
    /// <param name="value"></param>
    /// <returns>
    ///   If the passed in value matches an element within the enumeration or within a defined
    ///   map, the matching enumerated value is returned. Otherwise, the default enumerated value is returned.
    /// </returns>
    private static object GetEnumValue(PropertyInfo propertyInfo, string value)
    {
      var objectCache = ObjectCache.Instance;

      EnumValueMap[] enumValueMap = null;

      if(objectCache.ContainsEnumValueMap(propertyInfo))
      {
        enumValueMap = objectCache.RetrieveEnumValueMap(propertyInfo);
      }
      else
      {
        enumValueMap = (EnumValueMap[])Attribute.GetCustomAttributes(propertyInfo, typeof(EnumValueMap));
      }

      EnumValueMap map = null;

      if(enumValueMap.Length > 0)
      {
        map = enumValueMap.FirstOrDefault(m => m.DatabaseValue == value);
      }

      try
      {
        return map != null
          ? Enum.Parse(propertyInfo.PropertyType, map.EnumValue)
          : Enum.Parse(propertyInfo.PropertyType, value);
      }
      catch(Exception ex)
      {
        try
        {
          foreach(var val in from Enum val in Enum.GetValues(propertyInfo.PropertyType)
                             let fi = propertyInfo.PropertyType.GetField(val.ToString())
                             let attributes = (StringValueAttribute[])fi.GetCustomAttributes(
                               typeof(StringValueAttribute), false)
                             let attr = attributes[0]
                             where attr.StringValue == value
                             select val)
          {
            return Convert.ChangeType(val, propertyInfo.PropertyType);
          }
        }
        catch(Exception ex2)
        {
          throw new ArgumentException(
            "The value '" + value + "' is not contained in " + propertyInfo.PropertyType.Name, ex2);
        }

        throw new ArgumentException("The value '" + value + "' is not contained in " + propertyInfo.PropertyType.Name,
          ex);
      }
    }
  }
}
